package com.nf.mvc.argument;

import com.nf.mvc.MethodArgumentResolver;
import com.nf.mvc.support.function.BiFunctionEx;
import com.nf.mvc.util.ObjectUtils;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

import static com.nf.mvc.util.ClassUtils.cast;


/**
 * 此抽象解析器对某个类型及其数组，list等组成的类型矩阵(matrix)提供解析支持，这“某个类型”我这里称之为标量类型(Scalar),
 * 这种类型矩阵解析的要点是对标量类型进行解析，矩阵中其它类型的解析，比如数组类型，List类型的解析会间接调用标量类型的解析。
 *
 * <p>由于类型矩阵中非标量类型的解析逻辑是固定的，比如直接实例化一个ArrayList来实现对List矩阵类型的处理就可以了。
 * 如果某些解析器需要对这种类型组合提供支持的时候，可以考虑继承此类</p>
 *
 * <p>并不要求所有的参数解析器继承此类型，比如@RequestBody修饰的参数解析器就完全没有必要继承此类型,ServletApi的参数解析器也不需要继承此类</p>
 *
 * <p>当前的实现没有对标量类型的set集合类型进行实现</p>
 *
 * @author cj
 * @see RequestBodyMethodArgumentResolver
 * @see FileTypeMethodArgumentResolver
 * @see SimpleTypeMethodArgumentResolver
 * @see ServletApiMethodArgumentResolver
 */
public abstract class AbstractTypeMatrixMethodArgumentResolver implements MethodArgumentResolver {

    /**
     * 方法supports与resolveArgument最开始的实现是用if else，后面优化为三目运算符，
     * 虽然代码行数变短了，但三目运算符的表达式中由于调用方法传递的参数比较多，导致代码太长了，反而没有if else直观。
     * 所以，这里采用map的形式来优化
     *
     * <p>标量类型，标量类型数组，标量类型list三种类型解析对应的方法参数个数不一样，无法用一个统一的函数接口表示，
     * 而且由于标量类型解析是特殊的，因为其它类型解析都间接调用了标量类型解析，所以此map中不准备放置标量类型的信息</p>
     *
     * <p>剔除标量类型之后，方法签名刚好就一致了，所以map类型的设计如下，但由于解析方法都抛出了异常，
     * 不能使用{@link java.util.function.BiFunction},所以自己设计了一个支持抛出异常的类型{@link BiFunctionEx}</p>
     *
     * <p>map的key放置一个判断是否是某一个矩阵类型的方法，值是解析此矩阵类型的方法，所以当增加新的矩阵类型，比如标量类型的set解析时，
     * 只需要添加2个对应的方法，并放置到map中即可</p>
     *
     * <p>用map优化之后有一个潜在的好处就是:在此类构造函数中对map进行填充的代码可以一目了然的了解支持的类型矩阵有哪些，不用翻遍整个类才知晓</p>
     */
    private final Map<Predicate<MethodParameter>, BiFunctionEx<MethodParameter, Object[], Object>> typeMatrices = new HashMap<>();

    public AbstractTypeMatrixMethodArgumentResolver() {
        initTypeMatrices();
    }

    /**
     * 这里初始化类型矩阵非标量类型的判断与对应的解析方法
     * <p>子类可以重写此方法以便增加其它矩阵类型或者修改父类对数组与List的实现，这里把这4个方法设置为私有的，是因为没有必要更改实现</p>
     */
    protected void initTypeMatrices() {
        typeMatrices.put(this::isScalarTypeArray, this::resolveScalarTypeArray);
        typeMatrices.put(this::isScalarTypeList, this::resolveScalarTypeList);
    }

    @Override
    public boolean supports(MethodParameter parameter) {
        return isScalarType(parameter) ||
                typeMatrices.keySet().stream().anyMatch(k -> k.test(parameter));
    }

    /**
     * 这里用final修饰方法，是不想让子类重写，因为支持的情况目前就这么三种，逻辑是固定的,
     * 如果以后要支持其他类型,比如Set<T>这样的情况,只需要在此父类添加相关实现即可,子类是不需要额外再改动的
     *
     * @param parameter 方法参数
     * @param request   请求对象
     * @return 解析之后的结果值
     * @throws Exception 解析过程中可能抛出的异常
     */
    @Override
    public final Object resolveArgument(MethodParameter parameter, HttpServletRequest request) throws Exception {
        /*  请求中根本没有对应参数名的请求数据时，source可能就是null的，
          这里不直接依据source为null值提前结束方法的执行，是因为简单类型参数解析器还需要对这些null值进行一些额外的逻辑处理，
          比如获取参数上的注解RequestParam注解指定的默认值。数据源为null的处理逻辑应该由具体的子类去处理，并不应该在父类这里处理
         */
        Object[] source = getSource(parameter, request);
        // 如果source为null，会返回一个长度为0的空数组，toObjectArray方法并不是一定要调用的，调用可以避免空引用与数组索引越界的异常
        Object[] values = ObjectUtils.toObjectArray(source);

        if (isScalarType(parameter)) {
            Object value = Array.getLength(values) == 0 ? null : values[0];
            return resolveScalarType(parameter, parameter.getType(), value);
        }
        return typeMatrices.entrySet().stream()
                .filter(entry -> entry.getKey().test(parameter))
                .findFirst().orElseThrow(UnsupportedOperationException::new)
                .getValue().apply(parameter, values);
    }

    /**
     * 判断参数解析器是否支持指定的类型，通常是用来确定标量类型是什么类型即可，
     * 比如简单类型的参数解析器，其标量类型就是{@link com.nf.mvc.util.ClassUtils#isSimpleType(Class)}；
     * 文件类型的参数解析器，其标量类型就是Part类型或者{@link com.nf.mvc.file.MultipartFile}
     *
     * @param scalarType 标量类型
     * @return true表示支持对此类型的解析
     */
    protected abstract boolean supportsInternal(Class<?> scalarType);

    /**
     * 此方法用来获取请求中的数据源的，数据源主要是request.getParameterValues(name)与request.getParts()两大类<br/>
     * 这里通过这个方法抽象化了2种不同的数据获取方式
     *
     * @param parameter：方法参数，利用它可以获取参数名之内的信息
     * @param request：请求对象
     * @return 通常返回某个key下的数组数据，即便只有一个数据，也以数组的形式返回
     */
    protected abstract Object[] getSource(MethodParameter parameter, HttpServletRequest request);

    private boolean isScalarType(MethodParameter methodParameter) {
        return supportsInternal(methodParameter.getType());
    }

    /**
     * 此方法只负责标量（Scalar）类型的值进行解析，类型矩阵中的其它类型的解析都会间接调用此类型来进行解析
     *
     * @param parameter      方法参数，利用它可以获取参数的其它信息，比如参数上的注解
     * @param scalarType     标量类型,普通的标量、数组的成员类型或者泛型集合实参类型，比如List<String>的String
     * @param parameterValue 未解析的原始值
     * @return 解析后的值
     * @throws Exception 抛出解析过程中产生的异常
     */
    protected abstract Object resolveScalarType(MethodParameter parameter, Class<?> scalarType, Object parameterValue) throws Exception;

    private boolean isScalarTypeList(MethodParameter methodParameter) {
        return methodParameter.isList() && supportsInternal(methodParameter.getFirstActualArgument());
    }

    private <T> List<T> resolveScalarTypeList(MethodParameter parameter, Object[] values) throws Exception {
        Class<?> scalarType = parameter.getFirstActualArgument();
        List<T> list = new ArrayList<>();
        for (int i = 0; i < Array.getLength(values); i++) {
            list.add(cast(resolveScalarType(parameter, scalarType, Array.get(values, i))));
        }
        return list;
    }

    private boolean isScalarTypeArray(MethodParameter methodParameter) {
        return methodParameter.isArray() && supportsInternal(methodParameter.getComponentType());
    }

    /**
     * 这里如果把方法签名改为<T> T[] resolveScalarTypeArray(...)这种形式，那么泛型擦除会导致方法签名是返回Object[],
     * 那么在处理int[]这种简单类型数组作为控制器参数时，会导致报无法把简单类型数组转换为Object[]数组的异常的问题，所以没有把本类或这3个解析方法设计为泛型
     *
     * @param parameter 控制器方法参数
     * @param values    参数值
     * @return 返回解析后的单个参数的值
     * @throws Exception 解析过程中抛出的异常
     */
    private Object resolveScalarTypeArray(MethodParameter parameter, Object[] values) throws Exception {
        /* 如果数据源为null，那么length就=0，那么array就是一个长度为0的数组，并不会进入到循环里面执行真正的参数处理
        //不要写这样的代码，因为实例化后可能是一个int[]或者double[],是不能转换为Object[]的
        //Object[] values = (Object[]) Array.newInstance(scalarType, length); */

        Class<?> scalarType = parameter.getComponentType();
        int length = Array.getLength(values);
        Object array = Array.newInstance(scalarType, length);
        for (int i = 0; i < length; i++) {
            Array.set(array, i, resolveScalarType(parameter, scalarType, Array.get(values, i)));
        }
        return array;
    }
}
